package org.budo.jdbc.dao.ddl.auto;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

import javax.persistence.Table;
import javax.sql.DataSource;

import org.budo.druid.util.DruidUtil;
import org.budo.jdbc.dao.ddl.auto.dialect.Dialect;
import org.budo.jdbc.dao.ddl.auto.dialect.MySqlDialect;
import org.budo.jdbc.dao.ddl.auto.dialect.SQLiteDialect;
import org.budo.support.javax.sql.util.JdbcUtil;
import org.budo.support.javax.sql.util.ResultSetUtil;
import org.budo.support.lang.util.ListUtil;
import org.budo.support.lang.util.MapUtil;
import org.budo.support.lang.util.ReflectUtil;
import org.budo.support.spring.context.aware.RootApplicationContextRefreshedEventListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;

import com.alibaba.druid.proxy.jdbc.DataSourceProxy;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

/**
 * @author limingwei
 * @see org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean
 */
@Slf4j
@Getter
@Setter
@Order(Ordered.HIGHEST_PRECEDENCE) // 在 org.budo.warehouse.logic.producer.DataProducerLoaderImpl 前
public class BudoAutoDdlSupport extends RootApplicationContextRefreshedEventListener {
    private static final String RESOURCE_PATTERN = "/**/*.class";

    private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();

    private DataSource dataSource;

    private String packagesToScan;

    @Override
    protected void onRootApplicationContextRefreshedEvent(ContextRefreshedEvent contextRefreshedEvent) {
        log.info("#25 contextRefreshedEvent=" + contextRefreshedEvent + ", dataSource=" + this.getDataSource() + ", packagesToScan=" + this.getPackagesToScan());

        try {
            this.scanPackages();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @see org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean.scanPackages(Configuration)
     */
    private void scanPackages() throws Throwable {
        String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(this.getPackagesToScan()) + RESOURCE_PATTERN;
        Resource[] resources = this.resourcePatternResolver.getResources(pattern);
        MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);
        for (Resource resource : resources) {
            if (resource.isReadable()) {
                MetadataReader metadataReader = readerFactory.getMetadataReader(resource);
                ClassMetadata classMetadata = metadataReader.getClassMetadata();
                String type = classMetadata.getClassName();
                this.handleType(type);
            }
        }
    }

    private void handleType(String typeName) {
        Class<?> type = ReflectUtil.classForName(typeName);
        Table table = type.getAnnotation(Table.class);
        if (null == table) {
            log.error("#73 no @Table on " + typeName);
            return;
        }

        // 如果表不存在就建表
        String tableName = table.name();
        if (!this.tableExists(tableName)) {
            log.info("#86 " + tableName + " not exists, creating");
            this.createTable(tableName, type);
        }

        this.checkUpdate(tableName, type);
    }

    private void checkUpdate(String tableName, Class<?> type) {
        try {
            List<String> columns = this.columns(tableName);

            List<Field> fields = ReflectUtil.getAllFields(type);

            for (Field field : fields) {
                if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {
                    continue;
                }

                String columnName = this.dialect(this.getDataSource()).columnName(field);
                if (!columns.contains(columnName) && !columns.contains(columnName.replace('`', ' ').trim())) {
                    this.addColumn(field);
                }
            }
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    private void addColumn(Field field) {
        Dialect dialect = this.dialect(this.getDataSource());
        String sql = dialect.addColumn(field);
        log.info("#110 sql=" + sql + ", field=" + field);
        JdbcUtil.executeUpdate(this.getDataSource(), sql);
    }

    private List<String> columns(String tableName) throws SQLException {
        Connection connection = this.getDataSource().getConnection();
        DatabaseMetaData metaData = connection.getMetaData();
        ResultSet resultSet = metaData.getColumns(null, null, tableName, null);
        List<Map<String, Object>> list = ResultSetUtil.toMapList(resultSet);

        List<String> columns = ListUtil.toStringList_2(MapUtil.get(list, "COLUMN_NAME"));
        log.info("#99 size=" + ListUtil.size(list) + ", columns = " + columns + ", list=" + list);

        JdbcUtil.close(resultSet);
        JdbcUtil.close(connection);

        return columns;
    }

    private void createTable(String tableName, Class<?> type) {
        Dialect dialect = this.dialect(this.getDataSource());
        String sql = dialect.createTable(type);

        log.info("#110 type=" + type + ", sql=" + sql);
        JdbcUtil.executeUpdate(this.getDataSource(), sql);
    }

    private Dialect dialect(DataSource dataSource) {
        String dbType = DruidUtil.getDbType((DataSourceProxy) dataSource);
        if ("sqlite".equals(dbType)) {
            return new SQLiteDialect();
        }

        if ("mysql".equals(dbType)) {
            return new MySqlDialect();
        }

        throw new RuntimeException("#148 dataSource=" + dataSource + ", dbType=" + dbType);
    }

    private boolean tableExists(String tableName) {
        try {
            Connection connection = this.getDataSource().getConnection();
            DatabaseMetaData metaData = connection.getMetaData();
            ResultSet resultSet = metaData.getTables(null, null, tableName, null);
            Boolean next = ResultSetUtil.next(resultSet);
            JdbcUtil.close(resultSet);
            JdbcUtil.close(connection);
            return next;
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}